/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.dubbo.config;

import org.apache.dubbo.common.Constants;
import org.apache.dubbo.common.URL;
import org.apache.dubbo.common.config.CompositeConfiguration;
import org.apache.dubbo.common.config.Environment;
import org.apache.dubbo.common.config.InmemoryConfiguration;
import org.apache.dubbo.common.extension.ExtensionLoader;
import org.apache.dubbo.common.logger.Logger;
import org.apache.dubbo.common.logger.LoggerFactory;
import org.apache.dubbo.common.utils.ClassHelper;
import org.apache.dubbo.common.utils.CollectionUtils;
import org.apache.dubbo.common.utils.ReflectUtils;
import org.apache.dubbo.common.utils.StringUtils;
import org.apache.dubbo.config.support.Parameter;
import org.apache.dubbo.rpc.model.ConsumerMethodModel;

import java.io.Serializable;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

/**
 * Utility methods and public methods for parsing configuration
 *
 * @export
 */
public abstract class AbstractConfig implements Serializable {

  protected static final Logger logger = LoggerFactory.getLogger(AbstractConfig.class);
  private static final long serialVersionUID = 4267533505537413570L;

  /**
   * The maximum length of a <b>parameter's value</b>
   */
  private static final int MAX_LENGTH = 200;

  /**
   * The maximum length of a <b>path</b>
   */
  private static final int MAX_PATH_LENGTH = 200;

  /**
   * The rule qualification for <b>name</b>
   */
  private static final Pattern PATTERN_NAME = Pattern.compile("[\\-._0-9a-zA-Z]+");

  /**
   * The rule qualification for <b>multiply name</b>
   */
  private static final Pattern PATTERN_MULTI_NAME = Pattern.compile("[,\\-._0-9a-zA-Z]+");

  /**
   * The rule qualification for <b>method names</b>
   */
  private static final Pattern PATTERN_METHOD_NAME = Pattern.compile("[a-zA-Z][0-9a-zA-Z]*");

  /**
   * The rule qualification for <b>path</b>
   */
  private static final Pattern PATTERN_PATH = Pattern.compile("[/\\-$._0-9a-zA-Z]+");

  /**
   * The pattern matches a value who has a symbol
   */
  private static final Pattern PATTERN_NAME_HAS_SYMBOL = Pattern
      .compile("[:*,\\s/\\-._0-9a-zA-Z]+");

  /**
   * The pattern matches a property key
   */
  private static final Pattern PATTERN_KEY = Pattern.compile("[*,\\-._0-9a-zA-Z]+");

  /**
   * The legacy properties container
   */
  private static final Map<String, String> legacyProperties = new HashMap<String, String>();

  /**
   * The suffix container
   */
  private static final String[] SUFFIXES = new String[]{"Config", "Bean"};

  static {
    legacyProperties.put("dubbo.protocol.name", "dubbo.service.protocol");
    legacyProperties.put("dubbo.protocol.host", "dubbo.service.server.host");
    legacyProperties.put("dubbo.protocol.port", "dubbo.service.server.port");
    legacyProperties.put("dubbo.protocol.threads", "dubbo.service.max.thread.pool.size");
    legacyProperties.put("dubbo.consumer.timeout", "dubbo.service.invoke.timeout");
    legacyProperties.put("dubbo.consumer.retries", "dubbo.service.max.retry.providers");
    legacyProperties.put("dubbo.consumer.check", "dubbo.service.allow.no.provider");
    legacyProperties.put("dubbo.service.url", "dubbo.service.address");

    // this is only for compatibility
    DubboShutdownHook.getDubboShutdownHook().register();
  }

  /**
   * The config id
   */
  protected String id;
  protected String prefix;

  private static String convertLegacyValue(String key, String value) {
    if (value != null && value.length() > 0) {
      if ("dubbo.service.max.retry.providers".equals(key)) {
        return String.valueOf(Integer.parseInt(value) - 1);
      } else if ("dubbo.service.allow.no.provider".equals(key)) {
        return String.valueOf(!Boolean.parseBoolean(value));
      }
    }
    return value;
  }

  private static String getTagName(Class<?> cls) {
    String tag = cls.getSimpleName();
    for (String suffix : SUFFIXES) {
      if (tag.endsWith(suffix)) {
        tag = tag.substring(0, tag.length() - suffix.length());
        break;
      }
    }
    return StringUtils.camelToSplitName(tag, "-");
  }

  protected static void appendParameters(Map<String, String> parameters, Object config) {
    appendParameters(parameters, config, null);
  }

  @SuppressWarnings("unchecked")
  protected static void appendParameters(Map<String, String> parameters, Object config,
      String prefix) {
    if (config == null) {
      return;
    }
    Method[] methods = config.getClass().getMethods();
    for (Method method : methods) {
      try {
        String name = method.getName();
        if (ClassHelper.isGetter(method)) {
          Parameter parameter = method.getAnnotation(Parameter.class);
          if (method.getReturnType() == Object.class || parameter != null && parameter.excluded()) {
            continue;
          }
          String key;
          if (parameter != null && parameter.key().length() > 0) {
            key = parameter.key();
          } else {
            key = calculatePropertyFromGetter(name);
          }
          Object value = method.invoke(config);
          String str = String.valueOf(value).trim();
          if (value != null && str.length() > 0) {
            if (parameter != null && parameter.escaped()) {
              str = URL.encode(str);
            }
            if (parameter != null && parameter.append()) {
              String pre = parameters.get(Constants.DEFAULT_KEY + "." + key);
              if (pre != null && pre.length() > 0) {
                str = pre + "," + str;
              }
              pre = parameters.get(key);
              if (pre != null && pre.length() > 0) {
                str = pre + "," + str;
              }
            }
            if (prefix != null && prefix.length() > 0) {
              key = prefix + "." + key;
            }
            parameters.put(key, str);
          } else if (parameter != null && parameter.required()) {
            throw new IllegalStateException(
                config.getClass().getSimpleName() + "." + key + " == null");
          }
        } else if ("getParameters".equals(name)
            && Modifier.isPublic(method.getModifiers())
            && method.getParameterTypes().length == 0
            && method.getReturnType() == Map.class) {
          Map<String, String> map = (Map<String, String>) method.invoke(config, new Object[0]);
          if (map != null && map.size() > 0) {
            String pre = (prefix != null && prefix.length() > 0 ? prefix + "." : "");
            for (Map.Entry<String, String> entry : map.entrySet()) {
              parameters.put(pre + entry.getKey().replace('-', '.'), entry.getValue());
            }
          }
        }
      } catch (Exception e) {
        throw new IllegalStateException(e.getMessage(), e);
      }
    }
  }

  protected static void appendAttributes(Map<String, Object> parameters, Object config) {
    appendAttributes(parameters, config, null);
  }

  protected static void appendAttributes(Map<String, Object> parameters, Object config,
      String prefix) {
    if (config == null) {
      return;
    }
    Method[] methods = config.getClass().getMethods();
    for (Method method : methods) {
      try {
        Parameter parameter = method.getAnnotation(Parameter.class);
        if (parameter == null || !parameter.attribute()) {
          continue;
        }
        String name = method.getName();
        if (ClassHelper.isGetter(method)) {
          String key;
          if (parameter.key().length() > 0) {
            key = parameter.key();
          } else {
            key = calculateAttributeFromGetter(name);
          }
          Object value = method.invoke(config);
          if (value != null) {
            if (prefix != null && prefix.length() > 0) {
              key = prefix + "." + key;
            }
            parameters.put(key, value);
          }
        }
      } catch (Exception e) {
        throw new IllegalStateException(e.getMessage(), e);
      }
    }
  }

  protected static ConsumerMethodModel.AsyncMethodInfo convertMethodConfig2AyncInfo(
      MethodConfig methodConfig) {
    if (methodConfig == null || (methodConfig.getOninvoke() == null
        && methodConfig.getOnreturn() == null && methodConfig.getOnthrow() == null)) {
      return null;
    }

    //check config conflict
    if (Boolean.FALSE.equals(methodConfig.isReturn()) && (methodConfig.getOnreturn() != null
        || methodConfig.getOnthrow() != null)) {
      throw new IllegalStateException(
          "method config error : return attribute must be set true when onreturn or onthrow has been set.");
    }

    ConsumerMethodModel.AsyncMethodInfo asyncMethodInfo = new ConsumerMethodModel.AsyncMethodInfo();

    asyncMethodInfo.setOninvokeInstance(methodConfig.getOninvoke());
    asyncMethodInfo.setOnreturnInstance(methodConfig.getOnreturn());
    asyncMethodInfo.setOnthrowInstance(methodConfig.getOnthrow());

    try {
      String oninvokeMethod = methodConfig.getOninvokeMethod();
      if (StringUtils.isNotEmpty(oninvokeMethod)) {
        asyncMethodInfo.setOninvokeMethod(
            getMethodByName(methodConfig.getOninvoke().getClass(), oninvokeMethod));
      }

      String onreturnMethod = methodConfig.getOnreturnMethod();
      if (StringUtils.isNotEmpty(onreturnMethod)) {
        asyncMethodInfo.setOnreturnMethod(
            getMethodByName(methodConfig.getOnreturn().getClass(), onreturnMethod));
      }

      String onthrowMethod = methodConfig.getOnthrowMethod();
      if (StringUtils.isNotEmpty(onthrowMethod)) {
        asyncMethodInfo
            .setOnthrowMethod(getMethodByName(methodConfig.getOnthrow().getClass(), onthrowMethod));
      }
    } catch (Exception e) {
      throw new IllegalStateException(e.getMessage(), e);
    }

    return asyncMethodInfo;
  }

  private static Method getMethodByName(Class<?> clazz, String methodName) {
    try {
      return ReflectUtils.findMethodByMethodName(clazz, methodName);
    } catch (Exception e) {
      throw new IllegalStateException(e);
    }
  }


  protected static void checkExtension(Class<?> type, String property, String value) {
    checkName(property, value);
    if (StringUtils.isNotEmpty(value)
        && !ExtensionLoader.getExtensionLoader(type).hasExtension(value)) {
      throw new IllegalStateException(
          "No such extension " + value + " for " + property + "/" + type.getName());
    }
  }

  /**
   * Check whether there is a <code>Extension</code> who's name (property) is <code>value</code>
   * (special treatment is required)
   *
   * @param type The Extension type
   * @param property The extension key
   * @param value The Extension name
   */
  protected static void checkMultiExtension(Class<?> type, String property, String value) {
    checkMultiName(property, value);
    if (StringUtils.isNotEmpty(value)) {
      String[] values = value.split("\\s*[,]+\\s*");
      for (String v : values) {
        if (v.startsWith(Constants.REMOVE_VALUE_PREFIX)) {
          v = v.substring(1);
        }
        if (Constants.DEFAULT_KEY.equals(v)) {
          continue;
        }
        if (!ExtensionLoader.getExtensionLoader(type).hasExtension(v)) {
          throw new IllegalStateException(
              "No such extension " + v + " for " + property + "/" + type.getName());
        }
      }
    }
  }

  protected static void checkLength(String property, String value) {
    checkProperty(property, value, MAX_LENGTH, null);
  }

  protected static void checkPathLength(String property, String value) {
    checkProperty(property, value, MAX_PATH_LENGTH, null);
  }

  protected static void checkName(String property, String value) {
    checkProperty(property, value, MAX_LENGTH, PATTERN_NAME);
  }

  protected static void checkNameHasSymbol(String property, String value) {
    checkProperty(property, value, MAX_LENGTH, PATTERN_NAME_HAS_SYMBOL);
  }

  protected static void checkKey(String property, String value) {
    checkProperty(property, value, MAX_LENGTH, PATTERN_KEY);
  }

  protected static void checkMultiName(String property, String value) {
    checkProperty(property, value, MAX_LENGTH, PATTERN_MULTI_NAME);
  }

  protected static void checkPathName(String property, String value) {
    checkProperty(property, value, MAX_PATH_LENGTH, PATTERN_PATH);
  }

  protected static void checkMethodName(String property, String value) {
    checkProperty(property, value, MAX_LENGTH, PATTERN_METHOD_NAME);
  }

  protected static void checkParameterName(Map<String, String> parameters) {
    if (CollectionUtils.isEmptyMap(parameters)) {
      return;
    }
    for (Map.Entry<String, String> entry : parameters.entrySet()) {
      checkNameHasSymbol(entry.getKey(), entry.getValue());
    }
  }

  protected static void checkProperty(String property, String value, int maxlength,
      Pattern pattern) {
    if (StringUtils.isEmpty(value)) {
      return;
    }
    if (value.length() > maxlength) {
      throw new IllegalStateException(
          "Invalid " + property + "=\"" + value + "\" is longer than " + maxlength);
    }
    if (pattern != null) {
      Matcher matcher = pattern.matcher(value);
      if (!matcher.matches()) {
        throw new IllegalStateException(
            "Invalid " + property + "=\"" + value + "\" contains illegal " +
                "character, only digit, letter, '-', '_' or '.' is legal.");
      }
    }
  }

  protected static Set<String> getSubProperties(Map<String, String> properties, String prefix) {
    return properties.keySet().stream().filter(k -> k.contains(prefix)).map(k -> {
      k = k.substring(prefix.length());
      return k.substring(0, k.indexOf("."));
    }).collect(Collectors.toSet());
  }

  private static String extractPropertyName(Class<?> clazz, Method setter) throws Exception {
    String propertyName = setter.getName().substring("set".length());
    Method getter = null;
    try {
      getter = clazz.getMethod("get" + propertyName);
    } catch (NoSuchMethodException e) {
      getter = clazz.getMethod("is" + propertyName);
    }
    Parameter parameter = getter.getAnnotation(Parameter.class);
    if (parameter != null && StringUtils.isNotEmpty(parameter.key()) && parameter
        .useKeyAsProperty()) {
      propertyName = parameter.key();
    } else {
      propertyName = propertyName.substring(0, 1).toLowerCase() + propertyName.substring(1);
    }
    return propertyName;
  }

  private static String calculatePropertyFromGetter(String name) {
    int i = name.startsWith("get") ? 3 : 2;
    return StringUtils
        .camelToSplitName(name.substring(i, i + 1).toLowerCase() + name.substring(i + 1), ".");
  }

  private static String calculateAttributeFromGetter(String getter) {
    int i = getter.startsWith("get") ? 3 : 2;
    return getter.substring(i, i + 1).toLowerCase() + getter.substring(i + 1);
  }

  @Parameter(excluded = true)
  public String getId() {
    return id;
  }

  public void setId(String id) {
    this.id = id;
  }

  public void updateIdIfAbsent(String value) {
    if (StringUtils.isNotEmpty(value) && StringUtils.isEmpty(id)) {
      this.id = value;
    }
  }

  protected void appendAnnotation(Class<?> annotationClass, Object annotation) {
    Method[] methods = annotationClass.getMethods();
    for (Method method : methods) {
      if (method.getDeclaringClass() != Object.class
          && method.getReturnType() != void.class
          && method.getParameterTypes().length == 0
          && Modifier.isPublic(method.getModifiers())
          && !Modifier.isStatic(method.getModifiers())) {
        try {
          String property = method.getName();
          if ("interfaceClass".equals(property) || "interfaceName".equals(property)) {
            property = "interface";
          }
          String setter = "set" + property.substring(0, 1).toUpperCase() + property.substring(1);
          Object value = method.invoke(annotation);
          if (value != null && !value.equals(method.getDefaultValue())) {
            Class<?> parameterType = ReflectUtils.getBoxedClass(method.getReturnType());
            if ("filter".equals(property) || "listener".equals(property)) {
              parameterType = String.class;
              value = StringUtils.join((String[]) value, ",");
            } else if ("parameters".equals(property)) {
              parameterType = Map.class;
              value = CollectionUtils.toStringMap((String[]) value);
            }
            try {
              Method setterMethod = getClass().getMethod(setter, parameterType);
              setterMethod.invoke(this, value);
            } catch (NoSuchMethodException e) {
              // ignore
            }
          }
        } catch (Throwable e) {
          logger.error(e.getMessage(), e);
        }
      }
    }
  }

  /**
   * Should be called after Config was fully initialized. // FIXME: this method should be completely
   * replaced by appendParameters
   *
   * @see AbstractConfig#appendParameters(Map, Object, String)
   * <p>
   * Notice! This method should include all properties in the returning map, treat @Parameter
   * differently compared to appendParameters.
   */
  public Map<String, String> getMetaData() {
    Map<String, String> metaData = new HashMap<>();
    Method[] methods = this.getClass().getMethods();
    for (Method method : methods) {
      try {
        String name = method.getName();
        // 判断目标方法是否是获取元数据的方法，也即方法名必须是getter类型的，并且返回值必须是基础数据类型
        if (isMetaMethod(method)) {
          // 获取getter中的属性名称
          String prop = calculateAttributeFromGetter(name);
          String key;
          // 判断目标方法上是否使用了@Parameter注解，如果使用了，则通过该注解来获取属性key
          Parameter parameter = method.getAnnotation(Parameter.class);
          if (parameter != null && parameter.key().length() > 0 && parameter.useKeyAsProperty()) {
            key = parameter.key();
          } else {
            key = prop;
          }
          // treat url and configuration differently, the value should always present in configuration though it may not need to present in url.
          //if (method.getReturnType() == Object.class || parameter != null && parameter.excluded()) {
          if (method.getReturnType() == Object.class) {
            metaData.put(key, null);
            continue;
          }
          // 通过反射获取属性值，将其设置到metaData中
          Object value = method.invoke(this);
          String str = String.valueOf(value).trim();
          if (value != null && str.length() > 0) {
            metaData.put(key, str);
          } else {
            metaData.put(key, null);
          }
        } else if ("getParameters".equals(name)
            && Modifier.isPublic(method.getModifiers())
            && method.getParameterTypes().length == 0
            && method.getReturnType() == Map.class) {
          // 如果方法名为getParameters()，并且返回值类型为Map，那么就会将该返回值中的键值对都放到metaData中，
          // 这里需要单独处理是因为，前面的分支所处理的返回值类型是基础数据类型
          Map<String, String> map = (Map<String, String>) method.invoke(this, new Object[0]);
          if (map != null && map.size() > 0) {
//                            String pre = (prefix != null && prefix.length() > 0 ? prefix + "." : "");
            for (Map.Entry<String, String> entry : map.entrySet()) {
              metaData.put(entry.getKey().replace('-', '.'), entry.getValue());
            }
          }
        }
      } catch (Exception e) {
        throw new IllegalStateException(e.getMessage(), e);
      }
    }
    return metaData;
  }

  @Parameter(excluded = true)
  public String getPrefix() {
    return StringUtils.isNotEmpty(prefix) ? prefix
        : (Constants.DUBBO + "." + getTagName(this.getClass()));
  }

  public void setPrefix(String prefix) {
    this.prefix = prefix;
  }

  /**
   * TODO: Currently, only support overriding of properties explicitly defined in Config class,
   * doesn't support overriding of customized parameters stored in 'parameters'.
   */
  public void refresh() {
    try {
      CompositeConfiguration compositeConfiguration = Environment.getInstance()
          .getConfiguration(getPrefix(), getId());
      InmemoryConfiguration config = new InmemoryConfiguration(getPrefix(), getId());
      config.addProperties(getMetaData());
      if (Environment.getInstance().isConfigCenterFirst()) {
        // The sequence would be: SystemConfiguration -> ExternalConfiguration -> AppExternalConfiguration -> AbstractConfig -> PropertiesConfiguration
        compositeConfiguration.addConfiguration(3, config);
      } else {
        // The sequence would be: SystemConfiguration -> AbstractConfig -> ExternalConfiguration -> AppExternalConfiguration -> PropertiesConfiguration
        compositeConfiguration.addConfiguration(1, config);
      }

      // loop methods, get override value and set the new value back to method
      // 这里会依次调用setter方法，属性值通过在配置列表中依次进行获取，在配置列表中越靠前的配置就越先获取到，
      // 从而达到按照层级进行配置的优先设置
      Method[] methods = getClass().getMethods();
      for (Method method : methods) {
        if (ClassHelper.isSetter(method)) {
          try {
            String value = StringUtils
                .trim(compositeConfiguration.getString(extractPropertyName(getClass(), method)));
            // isTypeMatch() is called to avoid duplicate and incorrect update, for example, we have two 'setGeneric' methods in ReferenceConfig.
            if (StringUtils.isNotEmpty(value) && ClassHelper
                .isTypeMatch(method.getParameterTypes()[0], value)) {
              method
                  .invoke(this, ClassHelper.convertPrimitive(method.getParameterTypes()[0], value));
            }
          } catch (NoSuchMethodException e) {
            logger.info("Failed to override the property " + method.getName() + " in " +
                this.getClass().getSimpleName() +
                ", please make sure every property has getter/setter method provided.");
          }
        }
      }
    } catch (Exception e) {
      logger.error("Failed to override ", e);
    }
  }

  @Override
  public String toString() {
    try {
      StringBuilder buf = new StringBuilder();
      buf.append("<dubbo:");
      buf.append(getTagName(getClass()));
      Method[] methods = getClass().getMethods();
      for (Method method : methods) {
        try {
          if (ClassHelper.isGetter(method)) {
            String name = method.getName();
            String key = calculateAttributeFromGetter(name);
            Object value = method.invoke(this);
            if (value != null) {
              buf.append(" ");
              buf.append(key);
              buf.append("=\"");
              buf.append(value);
              buf.append("\"");
            }
          }
        } catch (Exception e) {
          logger.warn(e.getMessage(), e);
        }
      }
      buf.append(" />");
      return buf.toString();
    } catch (Throwable t) {
      logger.warn(t.getMessage(), t);
      return super.toString();
    }
  }

  /**
   * FIXME check @Parameter(required=true) and any conditions that need to match.
   */
  @Parameter(excluded = true)
  public boolean isValid() {
    return true;
  }

  private boolean isMetaMethod(Method method) {
    String name = method.getName();
    if (!(name.startsWith("get") || name.startsWith("is"))) {
      return false;
    }
    if ("get".equals(name)) {
      return false;
    }
    if ("getClass".equals(name)) {
      return false;
    }
    if (!Modifier.isPublic(method.getModifiers())) {
      return false;
    }
    if (method.getParameterTypes().length != 0) {
      return false;
    }
    if (!ClassHelper.isPrimitive(method.getReturnType())) {
      return false;
    }
    return true;
  }

  @Override
  public boolean equals(Object obj) {
    if (obj == null || !(obj.getClass().getName().equals(this.getClass().getName()))) {
      return false;
    }

    Method[] methods = this.getClass().getMethods();
    for (Method method1 : methods) {
      if (ClassHelper.isGetter(method1) && ClassHelper.isPrimitive(method1.getReturnType())) {
        Parameter parameter = method1.getAnnotation(Parameter.class);
        if (parameter != null && parameter.excluded()) {
          continue;
        }
        try {
          Method method2 = obj.getClass().getMethod(method1.getName(), method1.getParameterTypes());
          Object value1 = method1.invoke(this, new Object[]{});
          Object value2 = method2.invoke(obj, new Object[]{});
          if ((value1 != null && value2 != null) && !value1.equals(value2)) {
            return false;
          }
        } catch (Exception e) {
          return true;
        }
      }
    }
    return true;
  }
}
